﻿'HIDEasy
'Copyright (C) 2011 ASFX

'This program is free software; you can redistribute it and/or
'modify it under the terms of the GNU General Public License
'as published by the Free Software Foundation; either version 2
'of the License, or (at your option) any later version.

'This program is distributed in the hope that it will be useful,
'but WITHOUT ANY WARRANTY; without even the implied warranty of
'MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
'GNU General Public License for more details.

'You should have received a copy of the GNU General Public License
'along with this program; if not, write to the Free Software
'Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
Imports System.Runtime.InteropServices
Imports System.Text

<CLSCompliant(False)> _
Public Class HIDReport

#Region "Variables"
    Private _ReportId As Byte

    Private _HidDevice As HIDDevice
    Private _ReportType As ReportType
    Private _ReportSize As UShort
    Private _MaxDataListLength As UInt32

    Private _IsDataIndexedAndValued As Boolean = False
    Private _DataIndexedAndValued As New Dictionary(Of UShort, UInt32)
    Private _DataIndexAndValueNotSetInReport As New Dictionary(Of UShort, HIDClass.HID_ERROR_CODE)
    Private _IsReported As Boolean
    Private _ReportData As Byte() = {}
#End Region

#Region "Enums and Structures"

    Enum ReportType
        Input = 0
        Output = 1
        Feature = 2
    End Enum

#End Region

#Region "Creator"

    Public Sub New(ByVal ReportType As ReportType, _
                   ByVal HidDevice As HIDDevice, _
                   ByVal newReport As Byte())

        _IsReported = True
        _IsDataIndexedAndValued = False
        _HidDevice = HidDevice
        _ReportType = ReportType
        _MaxDataListLength = GetMaxDataListLength()
        _ReportData = newReport

        Select Case _ReportType
            Case HIDReport.ReportType.Input
                _ReportSize = _HidDevice.Capabilities.InputReportByteLength
            Case HIDReport.ReportType.Output
                _ReportSize = _HidDevice.Capabilities.OutputReportByteLength
            Case HIDReport.ReportType.Feature
                _ReportSize = _HidDevice.Capabilities.FeatureReportByteLength
        End Select
        If ReportData.Length = 0 Then
            Throw New HIDException("Unable to Create HIDReport (Report.length=0).")
        ElseIf _ReportData.Length <> _ReportSize Then
            Throw New HIDException("Unable to Create HIDReport (Report.length<>ReportSize).")
        End If
        _ReportData = newReport
        _ReportId = _ReportData(0)
    End Sub

    Public Sub New(ByVal ReportType As ReportType, _
                   ByVal HidDevice As HIDDevice, _
                   ByVal ReportID As Byte, _
                   ByVal newDataIndexedAndValue As Dictionary(Of UShort, UInt32))

        _IsReported = False
        _IsDataIndexedAndValued = True
        _HidDevice = HidDevice
        _ReportType = ReportType
        _MaxDataListLength = GetMaxDataListLength()

        If newDataIndexedAndValue.Count = 0 Then
            Throw New HIDException("Unable to create a HIDReport with DataIndexAndValue.Count=0")
        End If

        Select Case _ReportType
            Case HIDReport.ReportType.Input
                _ReportSize = _HidDevice.Capabilities.InputReportByteLength
            Case HIDReport.ReportType.Output
                _ReportSize = _HidDevice.Capabilities.OutputReportByteLength
            Case HIDReport.ReportType.Feature
                _ReportSize = _HidDevice.Capabilities.FeatureReportByteLength
        End Select
        _ReportId = ReportID
        _DataIndexedAndValued = newDataIndexedAndValue
    End Sub

#End Region

#Region "Properties"

    Public ReadOnly Property ReportId() As Byte
        Get
            Return _ReportId
        End Get
    End Property

    Public ReadOnly Property Type As ReportType
        Get
            Return _ReportType
        End Get
    End Property

    Public ReadOnly Property ReportData() As Byte()
        Get
            If Not _IsReported Then
                ParseDataIndexToReport()
                _IsReported = True
            End If
            Return _ReportData
        End Get
    End Property

    Public ReadOnly Property DataIndexAndValue() As Dictionary(Of UShort, UInt32)
        Get
            If Not _IsDataIndexedAndValued Then
                ParseReportToDataIndex()
                _IsDataIndexedAndValued = True
            End If
            Return _DataIndexedAndValued
        End Get
    End Property

    Public ReadOnly Property DataIndexAndValueNotSetInReport As Dictionary(Of UShort, HIDClass.HID_ERROR_CODE)
        Get
            If Not _IsDataIndexedAndValued Then
                ParseReportToDataIndex()
                _IsDataIndexedAndValued = True
            End If
            Return _DataIndexAndValueNotSetInReport
        End Get
    End Property

#End Region

#Region "Parse Report And DataIndex"

    Private Sub ParseReportToDataIndex()
        Dim _DataListLength As UInt32 = _MaxDataListLength
        If _DataListLength = 0 Then
            Throw New HIDException("Unable to Parse Data (HidP_MaxDataListLength=0)")
        End If
        Dim _list As HIDItemCollection = Nothing
        Select Case _ReportType
            Case ReportType.Input
                _list = _HidDevice.InputItems
            Case ReportType.Output
                _list = _HidDevice.OutputItems
            Case ReportType.Feature
                _list = _HidDevice.FeatureItems
        End Select
        ' Alloc Memory -> _MaxDataListLength* SizeOf(HIDP_DATA)
        Dim _HIDP_Data_Length As ULong = _DataListLength * CULng(Marshal.SizeOf(GetType(HIDClass.HIDP_DATA)))
        Dim _HIDP_Data_ptr As IntPtr
        Dim _HidDeviceData_ptr As IntPtr
        Try
            _HIDP_Data_ptr = Marshal.AllocHGlobal(CInt(_HIDP_Data_Length))
            _HidDeviceData_ptr = Marshal.AllocHGlobal(_ReportSize)

            For i As Integer = 0 To _ReportSize - 1
                Dim _ptr As IntPtr = New IntPtr(_HidDeviceData_ptr.ToInt64 + i)
                Marshal.WriteByte(_ptr, _ReportData(i))
            Next

            'Get HidP_GetData
            Dim _
                result As HIDClass.HID_ERROR_CODE = _
                    NativeMethods.HidP_GetData(CType(_ReportType, HIDClass.HIDP_REPORT_TYPE), _
                                               _HIDP_Data_ptr, _DataListLength, _HidDevice.DangerousGetPreparsedData, _
                                               _HidDeviceData_ptr, _ReportSize)

            If result <> HIDClass.HID_ERROR_CODE.HIDP_STATUS_SUCCESS Then
                Throw New HIDException("Unable to parse ReportData (HID_ERROR_CODE=" & result.ToString("g") & ")")
            End If

            For i As Integer = 0 To CInt(_DataListLength - 1)
                Dim _ptr As New IntPtr(_HIDP_Data_ptr.ToInt64 + i * Marshal.SizeOf(GetType(HIDClass.HIDP_DATA)))
                Dim _Data As HIDClass.HIDP_DATA
                _Data = CType(Marshal.PtrToStructure(_ptr, GetType(HIDClass.HIDP_DATA)), HIDClass.HIDP_DATA)
                _DataIndexedAndValued.Add(_Data.DataIndex, _Data.Values.RawValue)
            Next

        Catch ex As Exception
            Throw New HIDException("Unable to parse ReportData.", ex)
        Finally
            Marshal.FreeHGlobal(_HIDP_Data_ptr)
            Marshal.FreeHGlobal(_HidDeviceData_ptr)
        End Try
    End Sub

    Private Sub ParseDataIndexToReport()
        Dim _List As HIDItemCollection = Nothing
        Dim _Reportlength As UShort = 0

        Select Case _ReportType
            Case ReportType.Input
                _List = _HidDevice.InputItems
                _Reportlength = _HidDevice.Capabilities.InputReportByteLength
            Case ReportType.Output
                _List = _HidDevice.OutputItems
                _Reportlength = _HidDevice.Capabilities.OutputReportByteLength
            Case ReportType.Feature
                _List = _HidDevice.FeatureItems
                _Reportlength = _HidDevice.Capabilities.FeatureReportByteLength
        End Select

        Dim _Ptr_ReportData As IntPtr

        SyncLock _List.SyncRoot
            _Ptr_ReportData = Marshal.AllocHGlobal(_Reportlength)
            Try
                Dim _result As HIDClass.HID_ERROR_CODE
                _result = _
                    NativeMethods.HidP_InitializeReportForID(CType(_ReportType, HIDClass.HIDP_REPORT_TYPE), _ReportId, _
                                                             _HidDevice.DangerousGetPreparsedData, _Ptr_ReportData, _
                                                             _Reportlength)
                If _result <> HIDClass.HID_ERROR_CODE.HIDP_STATUS_SUCCESS Then
                    'Throw New System.HIDException("Unable to Initialize '" & _ReportType.ToString("g") & "' Report with ID=" & _ReportId.ToString & ".")
                    For i As Integer = 0 To _Reportlength - 1
                        Marshal.WriteByte(_Ptr_ReportData, i, 0)
                    Next
                    Marshal.WriteByte(_Ptr_ReportData, _ReportId)
                End If

                Dim _DataLength As UInt32 = 1
                If _DataIndexedAndValued.Count > 0 Then

                    For Each key As UShort In _DataIndexedAndValued.Keys
                        Dim _item As HIDItem = _List(_ReportId, key)
                        Dim _Hipd_Data As HIDClass.HIDP_DATA
                        With _Hipd_Data
                            .DataIndex = key
                            .Values.RawValue = _DataIndexedAndValued(key)
                        End With

                        If _item.IsButton AndAlso _item.ButtonIsOn Then
                            _result = _
                                SetData(_item, _Hipd_Data, CType(_ReportType, HIDClass.HIDP_REPORT_TYPE), _
                                        _Ptr_ReportData, _Reportlength)
                        Else
                            _result = HIDClass.HID_ERROR_CODE.HIDP_STATUS_SUCCESS
                        End If
                        Select Case _result
                            Case HIDClass.HID_ERROR_CODE.HIDP_STATUS_SUCCESS

                            Case HIDClass.HID_ERROR_CODE.HIDP_STATUS_BUTTON_NOT_PRESSED
                                '    'item.HasChanged = False

                                'Case HID_ERROR_CODE.HIDP_STATUS_BUFFER_TOO_SMALL
                                '    _DataIndexAndValueNotSetInReport.Add(_item.DataIndex, _result)
                                'If Not _item.IsButton Then
                                '    GoTo ErrorBufferTooSmallOnValue
                                'End If
                            Case Else
                                _DataIndexAndValueNotSetInReport.Add(_item.DataIndex, _result)
                                'ErrorBufferTooSmallOnValue:
                                'If _item Is Nothing Then
                                '    Throw New System.HIDException("Unable to set/unset Button/Value in Report (HID_ERROR_CODE=" & _
                                '                           _result.ToString("g") & " (Unable to find HIDItem).")
                                'Else
                                '    Throw New System.HIDException("Unable to set/unset Button/Value in Report (HID_ERROR_CODE=" & _
                                '                           _result.ToString("g") & ", HIDItem=" & _item.ToString.Replace(vbCrLf, ", ") & "). ")
                                'End If
                        End Select
                    Next
                End If
                ReDim Preserve _ReportData(_Reportlength - 1)
                For i As Integer = 0 To _Reportlength - 1
                    _ReportData(i) = Marshal.ReadByte(_Ptr_ReportData, i)
                Next
                _ReportId = _ReportData(0)
            Catch ex As Exception
                Throw New HIDException("Unable to 'ParseDataIndexToReport'", ex)
            Finally
                Marshal.FreeHGlobal(_Ptr_ReportData)
            End Try
        End SyncLock
    End Sub

#End Region

#Region "Set/Unset HIDItem"

    Private Function SetButtonUsage(ByVal item As HIDItem, _
                                    ByVal ReportType As HIDClass.HIDP_REPORT_TYPE, _
                                    ByVal Report As IntPtr, _
                                    ByVal ReportLength As UInt32) As HIDClass.HID_ERROR_CODE
        Dim _result As HIDClass.HID_ERROR_CODE
        Dim _ptr_Usage As IntPtr = Marshal.AllocHGlobal(Marshal.SizeOf(GetType(UShort)))
        Dim _Datalength As UInt32 = 1
        Try
            If item.Usage > Int16.MaxValue Then
                Dim _newvalue As UShort
                _newvalue = item.Usage
                Marshal.WriteInt16(_ptr_Usage, CShort(item.Usage))
            Else
                Marshal.WriteInt16(_ptr_Usage, CShort(item.Usage))
            End If
            _result = NativeMethods.HidP_SetUsages(ReportType, _
                                                   item.UsagePage, _
                                                   item.LinkCollection, _
                                                   _ptr_Usage, _
                                                   _Datalength, _
                                                   _HidDevice.DangerousGetPreparsedData, _
                                                   Report, _
                                                   ReportLength)
        Catch ex As Exception
            Throw New HIDException("Unable to 'SetButtonUsage'.", ex)
        Finally
            Marshal.FreeHGlobal(_ptr_Usage)
        End Try

        Return _result
    End Function

    Private Function UnsetButtonUsage(ByVal item As HIDItem, _
                                      ByVal ReportType As HIDClass.HIDP_REPORT_TYPE, _
                                      ByVal Report As IntPtr, _
                                      ByVal ReportLength As UInt32) As HIDClass.HID_ERROR_CODE
        Dim _result As HIDClass.HID_ERROR_CODE
        Dim _ptr_Usage As IntPtr = Marshal.AllocHGlobal(Marshal.SizeOf(GetType(UShort)))
        Dim _Datalength As UInt32 = 1
        Try
            If item.Usage > Int16.MaxValue Then
                Dim _newvalue As UShort
                _newvalue = item.Usage
                Marshal.WriteInt16(_ptr_Usage, CShort(item.Usage))
            Else
                Marshal.WriteInt16(_ptr_Usage, CShort(item.Usage))
            End If
            _result = NativeMethods.HidP_UnsetUsages(ReportType, _
                                                     item.UsagePage, _
                                                     item.LinkCollection, _
                                                     _ptr_Usage, _
                                                     _Datalength, _
                                                     _HidDevice.DangerousGetPreparsedData, _
                                                     Report, _
                                                     ReportLength)
        Catch ex As Exception
            Throw New HIDException("Unable to 'UnsetButtonUsage'.", ex)
        Finally
            Marshal.FreeHGlobal(_ptr_Usage)
        End Try

        Return _result
    End Function

    Private Function SetData(ByVal item As HIDItem, _
                             ByVal Hidp_Data As HIDClass.HIDP_DATA, _
                             ByVal ReportType As HIDClass.HIDP_REPORT_TYPE, _
                             ByVal Report As IntPtr, _
                             ByVal ReportLength As UInt32) As HIDClass.HID_ERROR_CODE
        Dim _result As HIDClass.HID_ERROR_CODE
        Dim _DataLength As UInt32 = 1
        Dim _PHidp_Data As IntPtr

        _PHidp_Data = Marshal.AllocHGlobal(Marshal.SizeOf(Hidp_Data))

        Try
            Marshal.StructureToPtr(Hidp_Data, _PHidp_Data, True)

            _result = NativeMethods.HidP_SetData(ReportType, _
                                                 _PHidp_Data, _
                                                 _DataLength, _
                                                 _HidDevice.DangerousGetPreparsedData, _
                                                 Report, _
                                                 ReportLength)
        Catch ex As Exception
            Throw New HIDException("Unable to 'SetData'.", ex)
        Finally
            Marshal.FreeHGlobal(_PHidp_Data)
        End Try
        Return _result
    End Function

    Private Function SetUsageValue(ByVal item As HIDItem, _
                                   ByVal Hidp_Data As HIDClass.HIDP_DATA, _
                                   ByVal ReportType As HIDClass.HIDP_REPORT_TYPE, _
                                   ByVal Report As IntPtr, _
                                   ByVal ReportLength As UInt32) As HIDClass.HID_ERROR_CODE
        Dim _result As HIDClass.HID_ERROR_CODE
        Dim _ptr_Usage As IntPtr = Marshal.AllocHGlobal(Marshal.SizeOf(GetType(UShort)))
        Dim _Datalength As UInt32 = 1
        Try
            If item.Usage > Int16.MaxValue Then
                Dim _newvalue As UShort
                _newvalue = item.Usage
                Marshal.WriteInt16(_ptr_Usage, CShort(item.Usage))
            Else
                Marshal.WriteInt16(_ptr_Usage, CShort(item.Usage))
            End If

            _result = NativeMethods.HidP_SetUsageValue(ReportType, _
                                                       item.UsagePage, _
                                                       item.LinkCollection, _
                                                       item.Usage, _
                                                       Hidp_Data.Values.RawValue, _
                                                       _HidDevice.DangerousGetPreparsedData, _
                                                       Report, _
                                                       ReportLength)
        Catch ex As Exception
            Throw New HIDException("Unable to 'SetUsageValue'.", ex)
        Finally
            Marshal.FreeHGlobal(_ptr_Usage)
        End Try

        Return _result
    End Function

#End Region

#Region "Misc"

    Private Function GetMaxDataListLength() As UInt32
        'Get HidP_MaxDataListLength()
        Dim _
            _Length As UInt32 = _
                NativeMethods.HidP_MaxDataListLength(CType(_ReportType, HIDClass.HIDP_REPORT_TYPE), _
                                                     _HidDevice.DangerousGetPreparsedData)
        If _Length = 0 Then
            Throw New HIDException("Unable to Get MaxDataListLength (HidP_MaxDataListLength=0).")
        End If
        Return _Length
    End Function

#End Region

#Region "Overrides Base Class (Object)"
    Public Overrides Function ToString() As String
        Dim _str As New StringBuilder()

        _str.AppendFormat("Type: {0:G}, ", Me.Type)
        _str.AppendFormat("Report Id: {0}, ", Me.ReportId)
        _str.AppendFormat("ParsedData: ({0} item(s), ", Me.DataIndexAndValue.Count)
        For Each _indexAndValue As KeyValuePair(Of UShort, UInteger) In Me.DataIndexAndValue
            _str.AppendFormat("[{0},{1}] ", _indexAndValue.Key, _indexAndValue.Value)
        Next
        If Me.DataIndexAndValueNotSetInReport.Count > 0 Then
            _str.AppendFormat("Error: ({0} item(s), ", Me.DataIndexAndValueNotSetInReport.Count)
            For Each _indexAndValue As KeyValuePair(Of UShort, HIDClass.HID_ERROR_CODE) In Me.DataIndexAndValueNotSetInReport
                _str.AppendFormat("[{0},{1}] ", _indexAndValue.Key, _indexAndValue.Value.ToString("g"))
            Next
        End If
        _str.AppendFormat(" RawData: ({0} item(s), ", Me.ReportData.Length)
        For Each _b As Byte In Me.ReportData
            _str.AppendFormat("[0x{0:X2}] ", _b)
        Next
        Return _str.ToString
    End Function
#End Region
End Class
